Detect and quarantine wedged vGPU VFs - #435
Open
yummybomb wants to merge 12 commits into
Open
Conversation
yummybomb
force-pushed
the
hypeship/vgpu-wedge-quarantine
branch
from
August 20, 2026 19:12
d312338 to
7d54fb9
Compare
yummybomb
marked this pull request as ready for review
August 20, 2026 19:33
yummybomb
force-pushed
the
hypeship/vgpu-wedge-quarantine
branch
from
August 20, 2026 20:17
7d54fb9 to
9a90223
Compare
yummybomb
force-pushed
the
hypeship/vgpu-wedge-quarantine
branch
from
August 20, 2026 21:57
9a90223 to
0ff16d1
Compare
There was a problem hiding this comment.
Cursor Bugbot has reviewed your changes using default effort and found 1 potential issue.
❌ Bugbot Autofix is OFF. To automatically fix reported issues with cloud agents, enable autofix in the Cursor dashboard.
Reviewed by Cursor Bugbot for commit 0ff16d1. Configure here.
yummybomb
force-pushed
the
hypeship/vgpu-wedge-quarantine
branch
from
August 20, 2026 22:04
0ff16d1 to
3db5546
Compare
Detection moves from host-side matching of the raw NVRM kernel line to an explicit guest-to-host report: the guest agent watches /dev/kmsg for the driver's RmInitAdapter failure and emits a HYPEMAN-GPU-INIT-FAILED marker over the established sentinel-marker channel, which the controller convicts on. The raw kernel line is no longer matched, so detection now requires the guest agent; images without it do not report. Quarantine hardening in the same pass: - The conviction brake pauses instead of drops: a suppressed conviction leaves the tail open and the agent's re-emission retries it once the window clears. - A match on an already-quarantined VF is not a new conviction: no brake accounting, no metric, no wedge-count inflation on controller restarts. - Tails reset when the instance acquires a new assignment, so a finished tail from a previous boot or VF cannot suppress scanning the next one. - A vf-health state file that fails to load refuses mutations (and retries the load) instead of letting the next conviction clobber every previously persisted quarantine. - Oversized unterminated log lines are skipped instead of re-buffered on every scan. - The controller idles on hosts without the vendor VFIO framework. GPU.md: DCGM quiesce is now an ordered step of the recovery sequence, and clearing vf-health.json documents the immediate-restart requirement.
Review fixes on the quarantine layer: - start now archives the previous boot's serial log before persisting the new vGPU assignment. The sentinel keys its tail on the assignment epoch, so the old ordering let a scan replay the previous boot's wedge report against the freshly assigned VF. A failed archive is fatal for GPU instances instead of a warning. - placement and profile availability refuse to run when the VF health state file exists but cannot be loaded, instead of treating the empty in-memory set as healthy and returning every quarantined VF to rotation. The load is retried on each attempt, so a repaired file self-heals. - a conviction whose persist fails is rolled back from memory: keeping it made the next report look like a repeat conviction, ending retries with nothing on disk. - the marker pattern requires the full shape through the quoted NVRM payload; a truncated or payload-less marker echoed by an exec command no longer matches. - unreadable instance metadata in the sentinel target listing logs a warning instead of silently shrinking detection coverage. - GPU.md recovery runbook unwound a circularity: placement excludes quarantined VFs and there is no VF-pin API, so the entry is cleared before the verification boot; the sentinel re-quarantines automatically if the cycle did not cure the VF.
The /dev/kmsg record priority encodes facility*8+level. Kernel printk is always facility 0 and the kernel assigns userspace writers LOG_USER or higher (a facility-0 prefix is coerced to LOG_USER, verified on a live 6.12 kernel), so requiring facility 0 makes in-guest forgery of the report impossible, matching the intended kernel-records-only semantics. Also retry a failed /dev/kmsg open instead of permanently disabling the watcher for the guest's lifetime.
The scan buffered each complete line whole, so guest console output could make the controller allocate line-sized buffers every pass; the 64KB cap only kept an unterminated tail from being re-read, and once such a line was skipped its late-arriving tail was parsed as a fresh line. Read through a fixed-size buffer instead: a line that overflows it cannot be a marker, so it is discarded — across scans if its newline has not arrived — without ever being held in memory, and its tail can no longer replay a marker. Rotation resets the skip state with the offset.
When the persisted state file fails to load, quarantine mutations are refused and vGPU placement fails closed, but the quarantined-VFs gauge reads zero from the empty in-memory set — exactly when quarantines exist and are unreadable. Export the load-failure state as its own gauge so the condition is alertable.
yummybomb
force-pushed
the
hypeship/vgpu-wedge-quarantine
branch
from
August 21, 2026 15:13
3db5546 to
b35501a
Compare
The serial console is a shared byte stream: kernel printk bypasses the tty buffer and can land mid-marker, and on a wedged VF the kernel is emitting NVRM errors exactly when the agent reports. A corrupted copy does not match the host's full-shape scan (deliberately, so echoed commands cannot convict), which delayed the report to the next 30s re-emission. Emit each report as three identical lines sharing one ts; the host convicts on the first intact copy and ignores the rest.
Auto-conviction no longer pauses on a burst. Systemic non-wedge init failures (e.g. a driver-mismatch image rollout) are expected to be caught on a test host before reaching production, and the convictions counter remains the alerting signal if one gets through; the brake was extra state and logic guarding against a case the rollout process already covers. Quarantine still only removes capacity and never touches instances, and the store's fail-closed load handling is unchanged.
Each /dev/kmsg read returns exactly one record and fails with EINVAL — without consuming the record — when the buffer is smaller. Records run up to CONSOLE_EXT_LOG_MAX (8 KiB), so bufio's default 4 KiB buffer wedged the watcher on the first oversized record: every reopen replayed the ring into the same record, silently losing all detection behind it. Size the buffer to the kernel's record bound and log non-EPIPE scan errors so a wedge is visible instead of silent.
A quarantine is only real once it is on disk, but the persist renamed without syncing the file or directory, so a host crash right after a conviction could silently drop it. Sync the temp file before the rename and the directory after, and deduplicate the sorted record listing.
The matched line is guest-controlled console bytes up to the 64 KiB line cap; logging and persisting it verbatim put up to that much guest output in error logs and vf-health.json. Keep just the marker match.
ClearVFQuarantine had no callers — the runbook documents editing vf-health.json directly — and WedgeCount was always 1. QuarantineVF's record return and IsVFQuarantined only served tests, and the sentinel's isQuarantined pre-check duplicated what QuarantineVF already reports via existed. The hostFramework indirection wrapped a single DiscoverVGPU call nothing injected.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.

Summary
Killing the process holding a vendor VFIO VF during guest driver init (~8s window) silently corrupts the VF: every later boot on it fails while sysfs, NVML, and the vGPU plugin all look healthy. #321 removed hypeman's own kill triggers; this layer detects the wedges external kills (OOM, operator, deploy restarts) still cause, and takes the VF out of placement.
Detection — guest agent report, no lifecycle change. On a wedged VF the guest driver is present, trying, and failing: its kernel loops
NVRM: ... RmInitAdapter failed!in the guest kernel log. The guest agent watches/dev/kmsgfor that line and reports it as aHYPEMAN-GPU-INIT-FAILEDmarker — the same guest→host channel as the existingHYPEMAN-*markers, landing in the instance'slogs/app.log. A newVGPUSentinelControllertails that file for every instance holding a vendor VFIO assignment and convicts on the marker; the full marker shape is required since a bare token could appear in echoed exec command lines. The agent only watches when an NVIDIA PCI function is present, throttles re-emission to one per 30s, and matches the full kernel-line shape, never the driver-build-specific(0x22:0x65:884)tuple. A no-driver image never produces the kernel line and can never be convicted. An image that skips the guest agent does not report — v1 accepts that coverage bound; the raw kernel line still reachesapp.logfor manual diagnosis.Quarantine. Convictions persist to
<data-dir>/gpu/vf-health.json(keyed by VF address, survives restarts) under the placement lock. One wedge produces one record no matter how many victim boots or controller restarts report it. The store fails closed on an unreadable state file: mutations are refused, and vGPU placement and advertised availability are disabled (with load retried on each attempt) rather than treating the empty in-memory set as healthy and returning quarantined VFs to rotation. A conviction whose persist fails is rolled back from memory so the next report retries it instead of reading as a repeat. Start archives the previous boot's serial log before persisting a new assignment (fatal on failure for GPU instances), so a scan can never replay the prior boot's report against a freshly assigned VF. There is no rate limit on convictions: a systemic non-wedge init failure (e.g. a driver-mismatch rollout) emits the same line on every VF and would quarantine the whole host, so such changes are validated on a test host first and the convictions counter is the alerting signal if one gets through.Placement.
Observability. Conviction logs at error level;
hypeman_instances_vgpu_sentinel_convictions_total, thehypeman_instances_vgpu_quarantined_vfsgauge, andhypeman_instances_vgpu_vf_health_store_unavailable(1 while the persisted state file cannot be loaded and placement is failing closed). The controller idles on hosts without the vendor VFIO framework.GPU.md documents the new behavior and the manual recovery flow (DCGM quiesce → SR-IOV cycle → restart services → verification boot → clear the VF's entry in
vf-health.json→ restart hypeman immediately).Out of scope (follow-up layer)
An operator force-cycle endpoint (DCGM quiesce →
sriov-manage -d/-e→ verification boot → clear quarantine). Until then recovery stays the documented manual runbook.Testing
go test -race ./lib/devices ./lib/paths ./lib/providers ./lib/system/guest_agentand the vGPU-relatedlib/instancestests pass locally;go vetclean. The fulllib/instancessuite fails only on environment-dependent VM/network tests, identically on the unmodified base.app.log) and the recovery sequence were validated on real L40S hardware by deliberately wedging VFs; the guest-agent watcher and the controller have not yet run against a live wedge end-to-end — that run should happen on the dev GPU host before this merges.Note
High Risk
Changes vGPU placement, advertised GPU capacity, and fail-closed behavior when VF health state is unreadable, so a bad store or a systemic init-failure signal can take a host out of GPU service. Guest-emitted markers can also shrink capacity (never mutate the instance).
Overview
Automatically detects wedged NVIDIA vGPU VFs (guest
RmInitAdaptertimeout with a healthy-looking host stack) and takes them out of placement until an operator SR-IOV-cycles the parent GPU.The guest agent watches
/dev/kmsgon NVIDIA guests and emits aHYPEMAN-GPU-INIT-FAILEDmarker. A newVGPUSentinelControllertails vendor-VFIO instanceapp.logs, quarantines the assigned VF ingpu/vf-health.json(fail-closed if the file is unreadable), and excludes those VFs from selection and advertised capacity. Parent GPUs with quarantined VFs become overflow-only; VF pick within a card is randomized so one undetected wedge no longer looks like a total GPU outage.Start now archives the previous boot log before a new vGPU assignment (fatal for GPU instances) so a prior marker cannot convict a fresh VF. Metrics cover convictions, quarantine count, and health-store unavailability. Recovery remains a documented manual DCGM quiesce + SR-IOV cycle + clear-quarantine restart.
Reviewed by Cursor Bugbot for commit 44b8a12. Bugbot is set up for automated code reviews on this repo. Configure here.